--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit f4d25a3bd8f530aecbe716933705d86dd49feffc
Parents : a96697b
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-26T06:41:17-05:00
feat: improve identity switching process with better feedback, reauthentication handling, and localized messages
Changes
15 files changed, 111 insertions(+), 28 deletions(-)
Diff
diff --git a/meshchatx.rsm b/meshchatx.rsm
index 963fd2bd..ee82a20e 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index a545ffd6..41ddfed4 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -1424,46 +1424,60 @@ export default {
},
async applyIdentitySwitched(json) {
const hash = json?.identity_hash;
+ const endSwitchUi = (aborted = false) => {
+ this.isSwitchingIdentity = false;
+ if (aborted) {
+ GlobalEmitter.emit("identity-switching-abort");
+ }
+ };
if (hash == null || hash === "") {
+ endSwitchUi(true);
return;
}
const now = Date.now();
if (this.identitySwitchDedupeHash === hash && now - this.identitySwitchDedupeAt < 10000) {
+ endSwitchUi(false);
return;
}
this.identitySwitchDedupeHash = hash;
this.identitySwitchDedupeAt = now;
- ToastUtils.success(this.$t("identities.switched"));
-
- if (json?.requires_reauth && GlobalState.authEnabled) {
- GlobalState.authenticated = false;
- try {
- await fetchCsrfToken(window.api);
- } catch {
- // Next mutating request will refresh CSRF when auth completes.
- }
- if (this.$route?.name !== "auth") {
- this.$router.push("/auth");
+ try {
+ if (json?.requires_reauth && GlobalState.authEnabled) {
+ ToastUtils.info(this.$t("identities.sign_in_after_switch"));
+ GlobalState.authenticated = false;
+ try {
+ await fetchCsrfToken(window.api);
+ } catch {
+ // Next mutating request will refresh CSRF when auth completes.
+ }
+ if (this.$route?.name !== "auth") {
+ this.$router.push("/auth");
+ }
+ endSwitchUi(true);
+ return;
}
- this.isSwitchingIdentity = false;
- GlobalEmitter.emit("identity-switched", json);
- return;
- }
- GlobalState.unreadConversationsCount = 0;
- GlobalState.missedCallsCount = 0;
- GlobalState.blockedDestinations = [];
+ ToastUtils.success(this.$t("identities.switched"));
- await this.getConfig();
- await this.updateRingtonePlayer();
- await this.getAppInfo();
- await this.getBlockedDestinations();
- this.updateTelephoneStatus();
+ GlobalState.unreadConversationsCount = 0;
+ GlobalState.missedCallsCount = 0;
+ GlobalState.blockedDestinations = [];
- this.isSwitchingIdentity = false;
+ await this.getConfig();
+ await this.updateRingtonePlayer();
+ await this.getAppInfo();
+ await this.getBlockedDestinations();
+ this.updateTelephoneStatus();
- GlobalEmitter.emit("identity-switched", json);
+ GlobalEmitter.emit("identity-switched", json);
+ } catch (e) {
+ console.error("applyIdentitySwitched failed", e);
+ ToastUtils.error(this.$t("identities.failed_switch"));
+ endSwitchUi(true);
+ return;
+ }
+ endSwitchUi(false);
},
onSyncPropagationNodeShell() {
this.syncPropagationNode();
diff --git a/meshchatx/src/frontend/components/TutorialModal.vue b/meshchatx/src/frontend/components/TutorialModal.vue
index 357de0d9..c3f1f74c 100644
--- a/meshchatx/src/frontend/components/TutorialModal.vue
+++ b/meshchatx/src/frontend/components/TutorialModal.vue
@@ -2363,6 +2363,7 @@ import AndroidStorageBridge from "../js/AndroidStorageBridge.js";
import ToastUtils from "../js/ToastUtils";
import DialogUtils from "../js/DialogUtils";
import GlobalState from "../js/GlobalState";
+import GlobalEmitter from "../js/GlobalEmitter";
import { bundledReticulumDocsUrl } from "../js/reticulumDocsEntryUrl.js";
import LanguageSelector from "./LanguageSelector.vue";
import { normalizeUiLocaleCode, setLocale } from "../js/localeLoader.js";
@@ -3466,6 +3467,7 @@ export default {
return true;
}
try {
+ GlobalEmitter.emit("identity-switching-start");
const response = await window.api.post("/api/v1/identities/switch", {
identity_hash: this.identityImportedHash,
});
@@ -3477,8 +3479,16 @@ export default {
ToastUtils.warning(this.$t("tutorial.identity_default_delete_failed"));
}
}
- if (response?.data?.hotswapped === false) {
+ if (response?.data?.hotswapped) {
+ GlobalEmitter.emit("identity-switched-apply", {
+ identity_hash:
+ response.data.identity_hash ?? this.identityImportedHash,
+ display_name: response.data.display_name ?? "",
+ requires_reauth: Boolean(response.data.requires_reauth),
+ });
+ } else if (response?.data?.hotswapped === false) {
ToastUtils.info(this.$t("identities.switch_scheduled"));
+ GlobalEmitter.emit("identity-switching-abort");
setTimeout(() => {
window.location.reload();
}, 1500);
@@ -3487,6 +3497,7 @@ export default {
return true;
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("tutorial.identity_switch_failed"));
+ GlobalEmitter.emit("identity-switching-abort");
return false;
}
},
diff --git a/meshchatx/src/frontend/components/layout/AppIdentitySwitchOverlay.vue b/meshchatx/src/frontend/components/layout/AppIdentitySwitchOverlay.vue
index d9ffce29..58723f33 100644
--- a/meshchatx/src/frontend/components/layout/AppIdentitySwitchOverlay.vue
+++ b/meshchatx/src/frontend/components/layout/AppIdentitySwitchOverlay.vue
@@ -21,7 +21,7 @@
{{ $t("app.loading_overlay_kicker") }}
</p>
<h2 class="mt-1.5 text-lg font-semibold tracking-tight text-gray-900 dark:text-white">
- {{ $t("app.loading_overlay_title") }}
+ {{ $t("app.switching_identity") }}
</h2>
<p class="mt-3 text-sm leading-relaxed text-gray-600 dark:text-zinc-400">
{{ $t("app.loading_overlay_subtitle") }}
diff --git a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
index 0b6583d2..ab0d9f46 100644
--- a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
+++ b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
@@ -473,11 +473,16 @@ export default {
mounted() {
this.getIdentities();
GlobalEmitter.on("identity-switched", this.onIdentitySwitched);
+ GlobalEmitter.on("identity-switching-abort", this.onIdentitySwitchAborted);
},
beforeUnmount() {
GlobalEmitter.off("identity-switched", this.onIdentitySwitched);
+ GlobalEmitter.off("identity-switching-abort", this.onIdentitySwitchAborted);
},
methods: {
+ onIdentitySwitchAborted() {
+ this.isCreating = false;
+ },
onIdentitySwitched() {
this.getIdentities();
this.isCreating = false;
@@ -688,6 +693,8 @@ export default {
});
} else {
ToastUtils.info(this.$t("identities.switch_scheduled"));
+ this.isCreating = false;
+ GlobalEmitter.emit("identity-switching-abort");
setTimeout(() => {
window.location.reload();
}, 2000);
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index 36baafd6..95229ebb 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "Zu Identität \"{name}\" wechseln?",
"delete_confirm": "Sind Sie sicher, dass Sie die Identität \"{name}\" löschen möchten? Dies kann nicht rückgängig gemacht werden.",
"switched": "Identität erfolgreich gewechselt.",
+ "sign_in_after_switch": "Identität gewechselt. Bitte melden Sie sich erneut an.",
"created": "Identität erfolgreich erstellt",
"deleted": "Identität gelöscht",
"failed_load": "Laden von Identitäten fehlgeschlagen",
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index a86a9081..4335a3f8 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -1016,6 +1016,7 @@
"switch_after_restore_confirm": "Switch to restored identity \"{name}\" now?",
"delete_confirm": "Delete \"{name}\"? This cannot be undone.",
"switched": "Identity switched successfully.",
+ "sign_in_after_switch": "Identity changed. Sign in again to continue.",
"created": "Identity created successfully",
"deleted": "Identity deleted",
"failed_load": "Failed to load identities",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index b413ca7b..f96f6a86 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "¿Cambiar a la identidad «{name}»?",
"delete_confirm": "¿Estás seguro de que quieres eliminar la identidad \"{name}\"? Esto no puede ser deshecho.",
"switched": "Identidad cambiada correctamente.",
+ "sign_in_after_switch": "Identidad cambiada. Inicia sesión de nuevo para continuar.",
"created": "Identidad creada con éxito",
"deleted": "Identidad eliminada",
"failed_load": "No se pudieron cargar las identidades",
diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index f30d3047..134b26ab 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "Vaihda identiteettiin \"{name}\"?",
"delete_confirm": "Haluatko varmasti poistaa identiteetin \"{name}\"? Toimintoa ei voi kumota.",
"switched": "Identiteetti vaihdettu.",
+ "sign_in_after_switch": "Identiteetti vaihdettu. Kirjaudu uudelleen jatkaaksesi.",
"created": "Identiteetti luotu",
"deleted": "Identiteetti poistettu",
"failed_load": "Identiteettien lataaminen epäonnistui",
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index b80921d5..75cea9ce 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "Passer à l'identité « {name} » ?",
"delete_confirm": "Voulez-vous vraiment supprimer l'identité \"{name}\" ? Cela ne peut être annulé.",
"switched": "Identité changée avec succès.",
+ "sign_in_after_switch": "Identité modifiée. Reconnectez-vous pour continuer.",
"created": "Identité créée avec succès",
"deleted": "Identité supprimée",
"failed_load": "Impossible de charger les identités",
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 6bd12072..7a1170df 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "Passare all'identità «{name}»?",
"delete_confirm": "Sei sicuro di voler eliminare l'identità \"{name}\"? L'azione non può essere annullata.",
"switched": "Identità cambiata con successo.",
+ "sign_in_after_switch": "Identità cambiata. Accedi di nuovo per continuare.",
"created": "Identità creata con successo",
"deleted": "Identità eliminata",
"failed_load": "Impossibile caricare le identità",
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 891c1656..f7c7e2f2 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "Overschakelen naar identiteit \"{name}\"?",
"delete_confirm": "Weet u zeker dat u identiteit \"{name}\" wilt verwijderen? Dit kan niet ongedaan worden gemaakt.",
"switched": "Identiteit succesvol gewisseld.",
+ "sign_in_after_switch": "Identiteit gewijzigd. Meld u opnieuw aan om door te gaan.",
"created": "Identiteit succesvol aangemaakt",
"deleted": "Identiteit verwijderd",
"failed_load": "Kon identiteiten niet laden",
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 473075ab..86be9428 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "Переключиться на личность «{name}»?",
"delete_confirm": "Вы уверены, что хотите удалить личность \"{name}\"? Это действие нельзя отменить.",
"switched": "Личность успешно изменена.",
+ "sign_in_after_switch": "Личность изменена. Войдите снова, чтобы продолжить.",
"created": "Личность успешно создана",
"deleted": "Личность удалена",
"failed_load": "Не удалось загрузить идентификаторы",
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index c8d92166..80bc84e1 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -1014,6 +1014,7 @@
"switch_confirm": "切换到身份「{name}」?",
"delete_confirm": "您确定要删除身份 \"{name}\" 吗?此操作无法撤销。",
"switched": "身份切换成功。",
+ "sign_in_after_switch": "身份已切换。请重新登录以继续。",
"created": "身份创建成功",
"deleted": "身份已删除",
"failed_load": "加载身份失败",
diff --git a/tests/frontend/AppIdentitySwitch.test.js b/tests/frontend/AppIdentitySwitch.test.js
index faa6d5a1..da984593 100644
--- a/tests/frontend/AppIdentitySwitch.test.js
+++ b/tests/frontend/AppIdentitySwitch.test.js
@@ -4,6 +4,10 @@ import ToastUtils from "../../meshchatx/src/frontend/js/ToastUtils";
import GlobalEmitter from "../../meshchatx/src/frontend/js/GlobalEmitter";
import GlobalState from "../../meshchatx/src/frontend/js/GlobalState";
+vi.mock("../../meshchatx/src/frontend/js/csrfToken.js", () => ({
+ fetchCsrfToken: vi.fn().mockResolvedValue(undefined),
+}));
+
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
default: {
success: vi.fn(),
@@ -88,6 +92,7 @@ describe("App.vue applyIdentitySwitched", () => {
expect(ctx.getConfig).toHaveBeenCalledTimes(1);
expect(ToastUtils.success).toHaveBeenCalledTimes(1);
expect(GlobalEmitter.emit).toHaveBeenCalledTimes(1);
+ expect(ctx.isSwitchingIdentity).toBe(false);
});
it("applies again for a different identity hash", async () => {
@@ -111,7 +116,8 @@ describe("App.vue applyIdentitySwitched", () => {
display_name: "X",
});
expect(ctx.getConfig).not.toHaveBeenCalled();
- expect(GlobalEmitter.emit).not.toHaveBeenCalled();
+ expect(GlobalEmitter.emit).toHaveBeenCalledWith("identity-switching-abort");
+ expect(ctx.isSwitchingIdentity).toBe(false);
});
it("no-ops when identity_hash is missing", async () => {
@@ -120,6 +126,8 @@ describe("App.vue applyIdentitySwitched", () => {
display_name: "X",
});
expect(ctx.getConfig).not.toHaveBeenCalled();
+ expect(GlobalEmitter.emit).toHaveBeenCalledWith("identity-switching-abort");
+ expect(ctx.isSwitchingIdentity).toBe(false);
});
it("re-applies same hash after dedupe window expires", async () => {
@@ -142,6 +150,40 @@ describe("App.vue applyIdentitySwitched", () => {
expect(GlobalEmitter.emit).toHaveBeenCalledTimes(1);
});
+ it("requires reauth without wiping UI via identity-switched", async () => {
+ GlobalState.authEnabled = true;
+ const ctx = {
+ ...makeCtx(),
+ $route: { name: "settings" },
+ $router: { push: vi.fn() },
+ };
+ await App.methods.applyIdentitySwitched.call(ctx, {
+ identity_hash: "h1",
+ requires_reauth: true,
+ });
+ expect(ToastUtils.info).toHaveBeenCalledWith("identities.sign_in_after_switch");
+ expect(ToastUtils.success).not.toHaveBeenCalled();
+ expect(ctx.getConfig).not.toHaveBeenCalled();
+ expect(GlobalEmitter.emit).toHaveBeenCalledWith("identity-switching-abort");
+ expect(GlobalEmitter.emit).not.toHaveBeenCalledWith(
+ "identity-switched",
+ expect.anything(),
+ );
+ expect(ctx.$router.push).toHaveBeenCalledWith("/auth");
+ expect(ctx.isSwitchingIdentity).toBe(false);
+ });
+
+ it("shows error and aborts when post-switch refresh fails", async () => {
+ const ctx = makeCtx();
+ ctx.getConfig.mockRejectedValueOnce(new Error("network"));
+ await App.methods.applyIdentitySwitched.call(ctx, {
+ identity_hash: "h1",
+ });
+ expect(ToastUtils.error).toHaveBeenCalledWith("identities.failed_switch");
+ expect(GlobalEmitter.emit).toHaveBeenCalledWith("identity-switching-abort");
+ expect(ctx.isSwitchingIdentity).toBe(false);
+ });
+
it("performance: dedupe path skips async work for many duplicate applies", async () => {
const ctx = makeCtx();
await App.methods.applyIdentitySwitched.call(ctx, {
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────